/*
 * @(#)WorkflowAutomaticTaskClassLoader.java
 *
 * Copyright (c) 2006 DCIVision Ltd
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of DCIVision
 * Ltd ("Confidential Information").  You shall not disclose such Confidential
 * Information and shall use it only in accordance with the terms of the license
 * agreement you entered into with DCIVision Ltd.
 */
package com.dcivision.workflow.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.dcivision.framework.SystemParameterConstant;
import com.dcivision.framework.SystemParameterFactory;
/**
 * <p>Class Name:    WorkflowAutomaticTaskClassLoader.java    </p>
  *
 * @author           tc //mod by greatwall shao
 * @company          DCIVision Limited
 * @creation date    2006-08-23
 */
public final class WorkflowAutomaticTaskClassLoader {

  protected Log       log           = LogFactory.getLog(this.getClass().getName());

  private String      compileClasspath;

  private ClassLoader parentClassLoader;

  private ArrayList   sourceDirs    = new ArrayList();

  // class name => LoadedClass
  private HashMap     loadedClasses = new HashMap();
  private HashMap     classes = new HashMap();

  public WorkflowAutomaticTaskClassLoader() {
    this(Thread.currentThread().getContextClassLoader());
  }

  public WorkflowAutomaticTaskClassLoader(ClassLoader parentClassLoader) {
    this(extractClasspath(parentClassLoader), parentClassLoader);

  }

  /**
   * @param compileClasspath
   *            used to compile dynamic classes
   * @param parentClassLoader
   *            the parent of the class loader that loads all the dynamic
   *            classes
   */
  public WorkflowAutomaticTaskClassLoader(String compileClasspath, ClassLoader parentClassLoader) {
    this.compileClasspath = compileClasspath;
    this.parentClassLoader = parentClassLoader;
  }

  /**
   * Add a directory that contains the source of dynamic java code.
   * 
   * @param srcDir
   * @return true if the add is successful
   */
  public boolean addSourceDir(File srcDir) {

    try {
      srcDir = srcDir.getCanonicalFile();
    } catch (IOException e) {
      // ignore
    }

    synchronized (sourceDirs) {

      // check existence
      for (int i = 0; i < sourceDirs.size(); i++) {
        SourceDir src = (SourceDir) sourceDirs.get(i);
        if (src.srcDir.equals(srcDir)) {
          return false;
        }
      }

      // add new
      SourceDir src = new SourceDir(srcDir);
      sourceDirs.add(src);

      info("Add source dir " + srcDir);
    }

    return true;
  }

  /**
   * Returns the up-to-date dynamic class by name.
   * 
   * @param className
   * @return
   * @throws ClassNotFoundException
   *             if source file not found or compilation error
   */
  public Class loadClass(String className) throws ClassNotFoundException {

    LoadedClass loadedClass = null;
    synchronized (loadedClasses) {
      loadedClass = (LoadedClass) loadedClasses.get(className);
    }

    // first access of a class
    if (loadedClass == null) {
      
      String resource = className.replace('.', '/') + ".java";
      SourceDir src = locateResource(resource);
      if (src == null) {
        Class clazz;
        synchronized (classes) {
          clazz  = (Class)classes.get(className);
        }
        if(clazz==null)
          try {
            clazz = new DynaClassLoader().loadClass(className);
          } catch (Exception e) {
            clazz=null;
          }
        if(clazz==null){
          throw new ClassNotFoundException("DynaCode class not found " + className);
        }
        classes.put(className, clazz);
       return clazz;
      }

      synchronized (this) {

        // compile and load class
        loadedClass = new LoadedClass(className, src);

        synchronized (loadedClasses) {
          loadedClasses.put(className, loadedClass);
        }
      }

      return loadedClass.clazz;
    }

    // subsequent access
    if (loadedClass.isChanged()) {
      // unload and load again
      unload(loadedClass.srcDir);
      return loadClass(className);
    }

    return loadedClass.clazz;
  }

  private SourceDir locateResource(String resource) {
    for (int i = 0; i < sourceDirs.size(); i++) {
      SourceDir src = (SourceDir) sourceDirs.get(i);
      if (new File(src.srcDir, resource).exists()) {
        return src;
      }
    }
    return null;
  }

  private void unload(SourceDir src) {
    // clear loaded classes
    synchronized (loadedClasses) {
      for (Iterator iter = loadedClasses.values().iterator(); iter.hasNext();) {
        LoadedClass loadedClass = (LoadedClass) iter.next();
        if (loadedClass.srcDir == src) {
          iter.remove();
        }
      }
    }

    // create new class loader
    src.recreateClassLoader();
  }

  /**
   * Get a resource from added source directories.
   * 
   * @param resource
   * @return the resource URL, or null if resource not found
   */
  public URL getResource(String resource) {
    try {

      SourceDir src = locateResource(resource);
      return src == null ? null : new File(src.srcDir, resource).toURL();

    } catch (MalformedURLException e) {
      // should not happen
      return null;
    }
  }

  /**
   * Get a resource stream from added source directories.
   * 
   * @param resource
   * @return the resource stream, or null if resource not found
   */
  public InputStream getResourceAsStream(String resource) {
    try {

      SourceDir src = locateResource(resource);
      return src == null ? null : new FileInputStream(new File(src.srcDir, resource));

    } catch (FileNotFoundException e) {
      // should not happen
      return null;
    }
  }

  /**
   * Create a proxy instance that implements the specified access interface
   * and delegates incoming invocations to the specified dynamic
   * implementation. The dynamic implementation may change at run-time, and
   * the proxy will always delegates to the up-to-date implementation.
   * 
   * @param interfaceClass
   *            the access interface
   * @param implClassName
   *            the backend dynamic implementation
   * @return
   * @throws RuntimeException
   *             if an instance cannot be created, because of class not found
   *             for example
   */
  public Object newProxyInstance(Class interfaceClass, String implClassName) throws RuntimeException {
    MyInvocationHandler handler = new MyInvocationHandler(implClassName);
    return Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[] { interfaceClass }, handler);
  }

  private class SourceDir {
    File           srcDir;

    File           binDir;

    Javac          javac;

    URLClassLoader classLoader;

    SourceDir(File srcDir) {
      this.srcDir = srcDir;

      this.binDir = new File(SystemParameterFactory.getSystemParameter(SystemParameterConstant.WORKFLOW_AUTOMATIC_TASK_BINDIR));
      if (!binDir.exists())
        this.binDir.mkdirs();

      // prepare compiler
      this.javac = new Javac(compileClasspath, binDir.getAbsolutePath());

      // class loader
      recreateClassLoader();
    }

    void recreateClassLoader() {
      try {
        classLoader = new URLClassLoader(new URL[] { binDir.toURL() }, parentClassLoader);
      } catch (MalformedURLException e) {
        // should not happen
      }
    }

  }

  private static class LoadedClass {
    String    className;

    SourceDir srcDir;

    File      srcFile;

    File      binFile;

    Class     clazz;

    long      lastModified;

    LoadedClass(String className, SourceDir src) {
      this.className = className;
      this.srcDir = src;

      String path = className.replace('.', '/');
      this.srcFile = new File(src.srcDir, path + ".java");
      this.binFile = new File(src.binDir, path + ".class");

      compileAndLoadClass();
    }

    boolean isChanged() {
      return srcFile.lastModified() != lastModified;
    }

    void compileAndLoadClass() {

      if (clazz != null) {
        return; // class already loaded
      }

      // compile, if required
      String error = null;
      if (binFile.lastModified() < srcFile.lastModified()) {
        error = srcDir.javac.compile(new File[] { srcFile });
      }

      if (error != null) {
        throw new AutoTaskCompileException("Failed to compile " + srcFile.getAbsolutePath() + ". Error: " + error);
      }

      try {
        // load class
        clazz = srcDir.classLoader.loadClass(className);

        // load class success, remember timestamp
        lastModified = srcFile.lastModified();

      } catch (ClassNotFoundException e) {
        throw new RuntimeException("Failed to load DynaCode class " + srcFile.getAbsolutePath());
      }

      info("Init " + clazz);
    }
  }

  private class MyInvocationHandler implements InvocationHandler {

    String backendClassName;

    Object backend;

    MyInvocationHandler(String className) {
      backendClassName = className;

      try {
        Class clz = loadClass(backendClassName);
        backend = newDynaCodeInstance(clz);

      } catch (ClassNotFoundException e) {
        throw new AutoTaskCompileException(e.toString());
      }
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

      // check if class has been updated
      Class clz = loadClass(backendClassName);
      if (backend.getClass() != clz) {
        backend = newDynaCodeInstance(clz);
      }

      try {
        // invoke on backend
        return method.invoke(backend, args);

      } catch (InvocationTargetException e) {
        throw e.getTargetException();
      }
    }

    private Object newDynaCodeInstance(Class clz) {
      try {
        return clz.newInstance();
      } catch (Exception e) {
        throw new RuntimeException("Failed to new instance of DynaCode class " + clz.getName(), e);
      }
    }

  }

  /**
   * Extracts a classpath string from a given class loader. Recognizes only
   * URLClassLoader.
   */
  private static String extractClasspath(ClassLoader cl) {
    StringBuffer buf = new StringBuffer();

    while (cl != null) {
      if (cl instanceof URLClassLoader) {
        URL urls[] = ((URLClassLoader) cl).getURLs();
        for (int i = 0; i < urls.length; i++) {
          if (buf.length() > 0) {
            buf.append(File.pathSeparatorChar);
          }
          buf.append(urls[i].getFile().toString());
        }
      }
      cl = cl.getParent();
    }

    return buf.toString();
  }

  /**
   * Log a message.
   */
  private static void info(String msg) {
    System.out.println("[HotDeploy]:" + msg);
  }
}
